本篇我會從 GraphQL 的精神: 查詢語言 (query language) 開始說起。
我自己剛學習時,常因分不清楚 schema 與 query 的 syntax 使用方式與時機而卡住,比如:
(問題解答附在文章末)
後來發現諸如此類問題的癥結點在於沒有釐清 query (資料查詢) 與 schema (架構設計) 的不同:一個是 client 送來的 request , 一個是 server 維護的架構,完全是兩個世界的東西!
以餐廳來比喻, 就像是客人 (client side) 照菜單 (Schema) 上的資訊來 **點餐 (發送 request)**一樣!
Query 是 client side 是符合 schema 規定的查詢語言格式 ;
Schema 是 server side 定義整體資料結構格式。
(可見 GraphQL 官方教學 右側的目錄也是先教 Query and Mutations 再教 Schemas and Types)
但為了讓大家在還沒寫出自己的 schema 之前可以練習 Query,請先打開我在第一天分享的連結 (傳送門: My GraphQL Playground Example)。
如果你覺得 GraphQL Playground 的 UI 太複雜不好看,可以複製 GraphQL Endpoint (也就是上面的 API 網址: https://z5r11749r7.lp.gql.zone/graphql) 然後到下載 GraphiQL 桌面程式 使用,效果如圖。
點開範例後,這是一張 query syntax 的說明圖:
接下來我會用 3 個範例來讓大家了解這張圖所表達的概念!
請大家在範例中 (左側)輸入:
query {
# 目前使用者
me {
id
name
}
}
右側可以得到
{
"data": {
"me": {
"id": 1,
"name": "Fong"
}
}
}
如圖:
接下來打開旁邊的 Schema 標籤會跑出 Documentation,長得像這樣:
兩者搭配之下可以觀察到:
在 Schema 中 name
field 的資料型別 (type) 為 String
,與取得的資料 ("Fong"
) 型別相符合
這種擁有實際值的 field 的型別 (type),我們稱為
Scalar Type
(基礎型別)。
其他基礎型別還包括Int
,Float
,String
,Boolean
,ID
。
me
這個 field 的資料型別 (type) 為 User
, User
是一種自定義的 Object Type
,展開後可得到一系列的 field
這種 自定義且 能展開的型別 (type) ,我們稱為
Object Type
。
#
是 query 中的備註,不會被 GraphQL Server 處理
到這裡大家的心中一樣可能會浮現一個疑惑:到底 User
跟 Object Type
以及 me
是什麼關係 ?
讓我們以 JAVA 物件導向的例子說明:
class Person { /* Defining your fields and methods */}
Person m = new Person();
我們的 Schema 與以上程式相比, Object Type
如同 Class
一樣是一種廣泛的概念,用於表達一個類型的超集,不會直接拿來使用。 User
就如同 Class
宣告的 Person
一樣,擁有著特定的資料結構,會使用來定義物件。而物件 m
跟 field me
的意思很像,都可以讓我們直接存取結構裡的資料!
Person 是一種 Class,而 m 是一個 Person class 的物件 ;
User 是一種 Object Type,而 me 是一個 User type 的 field 。
另外,GraphQL 的 Operation Type 預設為 query ,所以使用 query 的話可以省略第一行直接下 {
。
如同一個物件導向程式裡的物件定義的 field 可以是實際值或是 reference 到不同 class 的物件; GraphQL 的 Object Type
也可以透過 field 來指向相同或不同的 Object Type
。回到 GraphQL Playground ,再次輸入:
query {
# 目前使用者
me {
id
name
+ # 目前使用者的貼文
+ posts {
+ id
+ title
+ }
}
}
可以得到
{
"data": {
"me": {
"id": "1",
"name": "Fong",
"posts": [
{
"id": "1",
"title": "Hello World!!"
},
{
"id": "4",
"title": "Love U Too"
}
]
}
}
}
如圖:
請再打開 Schema 並點選 me
-> User.post
-> Post
:
兩者搭配之下可以觀察到:
posts
這個 field 的 type 為 [Post]
,代表得到的資料格式為一個 Post
的 Array
如果展開的是 Array type 的 field ,那就會以 json array 的方式(
[]
)呈現
query 到 Object Type
的 field 時一定要展開並選取 fields!
GraphQL Server 會依據 query 選取的 field 給予 實際值,所以如果 Object Type
不展開 GraphQL Server 會給不出實際值。
query 結構最末端的 field 一定要是
Scalar Type
Object Type
使用 field 互相連接!
在 schema 中可發現 User
type 的 friends
field 是 [User]
type ,而 Post
type 的 field author
也是 User
type。
所以你可以做出這樣的操作:「取得我所有朋友的朋友的貼文的作者的生日」XD,這也是 GraphQL 有趣且強大的地方,一次拿到所有資料!
PS 對安全性有警覺性的朋友一定有發現「如果 client 端傳來無限深的 query 搞爆 server 怎麼辦 ?」,別擔心,之後會提到一些解決方法!
看完以上兩個例子,我們可以得到一個重要的結論:
GraphQL query 基本上就是選取 Object 的 field 來獲取資料
請寫出正確的 query 取得 「我的朋友們的朋友們的貼文裡,那些按讚的人們的名字」,
並告訴我「我第一個朋友的第一個朋友的第一則貼文的第一個按讚的人是誰?」
解答: 點開附圖
如果你在以上的過程中輸錯過字或是沒展開 Object Type
的 field 的話, GraphQL Playground 的就會亮起紅燈 (紅色底線) 以阻止你送出錯誤的 Query。若是你忽視警告硬要闖紅燈送出 Query 的話, 就會得到 Error,如圖:
me
後面沒有打上 {
:
Cannot query field \"nmae\" on type \"User\". Did you mean \"name\"?
Object Type
: Field \"friends\" of type \"[User]\" must have a selection of subfields. Did you mean \"friends { ... }\"?
而到底 GraphQL 是怎麼做到的呢?我來簡短講解一下背後的原理:
GraphQL Server 收到 Query 後
先 解析 (Parse) 這段 query 成 AST (abstract syntax tree ,一種正規化的資料結構)
如果有 syntax error (少加括號或 keyword 打錯),立即回傳 Error,如錯誤 1 。
第一步通過後,進入 驗證(Validation) 階段
此時 GraphQL 會詳細檢查 field 的細項,如以上提到的兩個範例,如果有錯立即回傳 Error,如錯誤 2 跟 3
前面兩步都通過後,才會進入 執行 (Execution) 階段。
工作會由 GraphQL Server 轉交給真正的 Server 去處理資料 (做計算或從 DB 拿資料)。
而回傳時 GraphQL 會將資料轉換成 Schema 中對應的格式,如果無法成功轉換 (如 String
轉 Int
),會立即回傳 Error 。
當然,也有可能發生問題如權限不足、資料取得失敗或其他商業邏輯引起的錯誤等等,此時也會回傳 Error
延伸閱讀: GraphQL explained
延伸閱讀: GraphQL Learn Validation & Execution
在這邊強調,一般來說
GraphQL 的 Query 及之後會提到的 Mutation 都是使用
POST
送出 !
包含以上的 IDE 背後也都是使用 POST !有人會說,為什麼 Query 不像 RESTful API 使用 GET 呢 ? 原因很簡單:因為 POST 才能自帶 body 啊!
以上 query 的內容其實就是 POST 的 body。
大家可以在 GraphQL Playground 送出 Query 前打開瀏覽器的開發者工具 (或對畫面點右鍵選 Inspect),
進入到 Network 的 tab 選 All 後,送出 Query 看第二筆紀錄,如圖:
可以看到他的 payload 其實就是一個 JSON,所以如果像前端要用 fetch
的話會像是這樣 (可直接在開發者工具的 console 輸入):
fetch('https://z5r11749r7.lp.gql.zone/graphql', {
body: JSON.stringify({ query: "{\n me {\n id\n name\n }\n}\n" }),
headers: {
'content-type': 'application/json' // 這一欄一定要設定!
},
method: 'POST',
})
.then(response => response.json()) // 輸出成 json
.then(data => console.log(data))
結果如圖:
PS 當然 query 也可以使用 GET
送出,但真的很難用也浪費了 GraphQL 優雅的設計,可參考 GraphQL Learn: Serving Over Http
開頭問題解答:
看完今天的文章,想必各位都已經對於 GraphQL 的 query 有了基本的認知,眾所期待的 GraphQL Server 實作將會在明天推出!手癢想打 code 的朋友們千萬別錯過!
這篇以後會比較難,如果有任何不懂的地方歡迎底下留言,我會盡力幫忙解惑!